package org.jsequences.reflect;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import com.google.common.base.Throwables;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.TypeToken;

/**
 * This is the abstract base class of all reflective Functions, Predicates and
 * others. It performs the typechecking for methods and constructors and ensures
 * that subclasses which use the same function can be equal. This class is
 * suitable to define simple wrappers for functional interfaces which do
 * <b>not</b> use checked exceptions. Trying to use this class for methods or
 * constructors which declare checked exceptions will result in
 * IllegalArgumentExceptions, and there is currently on purpose no way around
 * this problem.
 * <p>
 * Please note that this is currently just a helper class, since it internal
 * workings might still need to be changed over time.
 * 
 * @param <T>
 *            the class of the method or constructor
 * @param <R>
 *            the return type of a method, [T] for a constructor
 * @author Eric Giese
 */
abstract class ReflectionMethod<T, R> {

	/**
	 * The internal method. Must be set in the constructor by calling either
	 * initConstructor or initMethod.
	 */
	private Invokable<T, R> method;

	/**
	 * The empty default constructor. Subclasses MUST call exactly one of the
	 * methods initConstructor, initStaticMethod or initMethod once in the
	 * constructor. Otherwise, all Operations of the ReflectionMethod will
	 * result in NullPointerExceptions. Multiple calls of the init-Methods will
	 * result in IllegalStateExceptions.
	 */
	protected ReflectionMethod() {
	}

	/**
	 * Returns the hashcode of the Method.
	 * 
	 * @see Method#hashCode()
	 */
	@Override
	public final int hashCode() {
		return method.hashCode();
	}

	/**
	 * Checks if the other object is an instance of this class and contains the
	 * same method.
	 * 
	 * @see Method#equals(Object)
	 */
	@Override
	public final boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		if (other instanceof ReflectionMethod) {
			ReflectionMethod<?, ?> that = (ReflectionMethod<?, ?>) other;
			return that.canEqual(this) && this.method.equals(that.method);
		}
		return false;
	}

	/**
	 * This method must be implemented by subclasses to determine if the other
	 * object can be equal to this one, considering inheritance.
	 * 
	 * This pattern allows it to declare covariant equals implementations and is
	 * used mainly in scala, but <a
	 * href="http://www.artima.com/lejava/articles/equality.html">can be used in
	 * java as well</a>.
	 */
	protected abstract boolean canEqual(Object other);

	/**
	 * Returns the Method Signature.
	 * 
	 * @see Method#toString
	 */
	@Override
	public final String toString() {
		return method.toString();
	}

	/**
	 * Initializes the ReflectionMethod to the given constructor with the given
	 * parameters.
	 * 
	 * @param parameters
	 *            the parameter types array
	 * @throws IllegalArgumentException
	 *             if the validation of the signature failed
	 * @throws IllegalStateException
	 *             if one of the init-Methods had already been called
	 */
	protected final void initConstructor(Class<?>... parameters) {
		checkState(method == null, "Method can be initialized only once!");
		TypeToken<T> classType = getClassType();
		try {
			@SuppressWarnings("unchecked")
			Invokable<T, R> invokable = (Invokable<T, R>) classType
					.constructor(classType.getRawType().getDeclaredConstructor(
							parameters));
			method = invokable;
		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException(e);
		}
		checkSignature();
	}

	/**
	 * Initializes the ReflectionMethod to the given instance method with the
	 * given types and parameters.
	 * 
	 * @param name
	 *            the name of the method
	 * @param parameters
	 *            the parameter types array
	 * @throws IllegalArgumentException
	 *             if the validation of the signature failed
	 * @throws IllegalStateException
	 *             if one of the init-Methods had already been called
	 */
	protected final void initInstanceMethod(String name, Class<?>... parameters) {
		initMethod(name, parameters);
		checkArgument(!method.isStatic(), "Method %s must not be static!",
				method);
	}

	/**
	 * Initializes a ReflectionMethod for the given static method with the given
	 * types and parameters.
	 * 
	 * @param name
	 *            the name of the method
	 * @param parameters
	 *            the parameter types array
	 * @throws IllegalArgumentException
	 *             if the validation of the signature failed
	 * @throws IllegalStateException
	 *             if one of the init-Methods had already been called
	 */
	protected final void initStaticMethod(String name, Class<?>... parameters) {
		initMethod(name, parameters);
		checkArgument(method.isStatic(), "Method %s must be static!", method);
	}

	/**
	 * Internal helper for both initStaticMethod and initMethod.
	 */
	private void initMethod(String name, Class<?>... parameters) {
		checkState(method == null, "Method can be initialized only once!");
		TypeToken<T> classType = getClassType();
		try {
			@SuppressWarnings("unchecked")
			Invokable<T, R> invokable = (Invokable<T, R>) classType
					.method(classType.getRawType().getDeclaredMethod(name,
							parameters));
			method = invokable;
		} catch (NoSuchMethodException e) {
			throw new IllegalStateException(
					"An error occured while loading the reflective function:"
							+ classType + "." + name
							+ Arrays.toString(parameters) + "!"
							+ "Please check the name and the parameters!");
		}
		checkSignature();
	}

	/**
	 * Call the internally wrapped method with the given instance and the
	 * parameters. This method does not raise checked exceptions! The
	 * premeleminary checks have been done, and the caller has to make sure that
	 * he only uses the parameter types which he had declared in initConstructor
	 * or initMethod.
	 * <p>
	 * 
	 * @param instance
	 *            the instance of the class whose method should be invoked. this
	 *            <b>must</b> be null for constructors and static methods.
	 * @param parameters
	 *            all parameters of the method to invoke. must match the
	 *            parameter types given during initialization.
	 * @throws SecurityException
	 *             if an IllegalAcessException occured
	 * @throws RuntimeException
	 *             if an InvocationTargetException occured. Only the exception
	 *             cause of said exception will be returned. If it is a
	 *             RuntimException or an Error, then it is thrown directly,
	 *             otherwise it is packed into a RuntimeException and then
	 *             thrown.
	 */
	protected final R invoke(T instance, Object... parameters) {
		try {
			return method.invoke(instance, parameters);
		} catch (IllegalAccessException e) {
			throw new SecurityException(e);
		} catch (InvocationTargetException e) {
			throw Throwables.propagate(e.getCause());
		}
	}

	/**
	 * Checks the method signature for validity.
	 * <ol>
	 * <li>The ReturnType must match the excepted returntype. Primitive Wrappers
	 * are taken into consideration.</li>
	 * <li>The method signature MUST NOT declare Checked Exceptions in their
	 * throws-clauses. RuntimeExceptions and Errors are, however, ok. Reason:
	 * This would break typesafety and is not needed.</li>
	 * </ol>
	 */
	private void checkSignature() {
		method.setAccessible(true);
		// Check Return type
		TypeToken<?> retType = method.getReturnType();
		if (Primitives.allPrimitiveTypes().contains(retType.getRawType())) {
			retType = TypeToken.of(Primitives.wrap(retType.getRawType()));
		}
		TypeToken<R> resultType = new TypeToken<R>(getClass()) {
		};
		checkArgument(retType.equals(resultType),
				"%s: ReturnType %s must be equal to %s", method, retType,
				resultType);

		// Check exceptions
		for (TypeToken<?> e : method.getExceptionTypes()) {
			Class<?> exType = e.getRawType();
			if (RuntimeException.class.isAssignableFrom(exType)
					|| Error.class.isAssignableFrom(exType)) {
				continue;
			}
			throw new IllegalArgumentException(
					method
							+ " must not declare checked exceptions"
							+ " as the surrounding Function is not allowed to throw them!");
		}
	}

	/** Returns the Runtime-TypeToken of the Class. */
	private TypeToken<T> getClassType() {
		return new TypeToken<T>(getClass()) {
		};
	}
}
